package org.brownflat.dbinit;

import org.apache.log4j.Logger;
import org.brownflat.dbinit.upgrade.Upgrade;
import org.brownflat.dbinit.upgrade.UpgradeSource;
import org.brownflat.dbinit.upgrade.UpgradeStatement;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

/**
 * <p>Database Init &copy Rupert Jones 2013</p>
 *
 * @author rup
 */
public class DefaultUpgradeRunner implements UpgradeRunner {
    private UpgradeSource source;
    private PatchSequenceManager sequenceManager;
    private DataSource dataSource;
    private final static Logger LOG = Logger.getLogger(UpgradeRunner.class);
    private boolean executable = false;

    public DefaultUpgradeRunner(UpgradeSource source, PatchSequenceManager sequenceManager, DataSource dataSource) {
        this.source = source;
        this.dataSource = dataSource;
        this.sequenceManager = sequenceManager;

        if(smokeTest()) {
           executable = true;
        } else {
            LOG.warn("Smoke test on upgrade running failed.");
        }
    }

    public void run() {
        LOG.info("*************************************************");
        LOG.info("Starting Database Upgrade Runner");
        LOG.info("*************************************************");
        int currentVersion = sequenceManager.getCurrentSequence();
        LOG.info("Current database patch version: " + currentVersion);
        List<Upgrade> remainingUpgrades = source.getUpgradesFrom(currentVersion);
        LOG.info("Will apply " + remainingUpgrades.size() + " upgrades");
        int count = 0;
        for(Upgrade upgrade : remainingUpgrades) {
            runUpgrade(upgrade);
            sequenceManager.markAsApplied(upgrade);
            count++;
        }

        LOG.info("All upgrades applied, patch version: " + sequenceManager.getCurrentSequence());
    }

    @Override
    public boolean isExecutable() {
        return executable;
    }

    private boolean smokeTest() {
        try {
            Connection connection = dataSource.getConnection();
            if(connection != null) {
                close(connection);
            }
            return true;
        } catch (Throwable throwable) {
            LOG.error(throwable);
            return false;
        }
    }

    private void runUpgrade(Upgrade upgrade) {
        LOG.info(" -- Applying upgrade " + upgrade.getName());
        List<UpgradeStatement> statements = upgrade.getSql();

        for(UpgradeStatement statement : statements) {
            String[] commands = statement.getSql().split(";");
            for(String command : commands) {
                if(!command.startsWith("--")) {
                    runCommand(upgrade, command);
                }
            }
        }
        LOG.info(" -- Upgrade applied [" + upgrade.getSequence() + "]");
    }

    private void runCommand(Upgrade upgrade, String command) {
        if(command.trim().length() == 0) {
            return;
        }
        Connection connection = null;
        Statement statement = null;
        try {
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);
            statement = connection.createStatement();
            statement.execute(command);
            connection.commit();
        } catch (Exception e) {
            try {
                connection.rollback();
            } catch (SQLException e1) {
                LOG.fatal("Problem whilst rolling back execution of statement " + command + " for upgrade " +  upgrade.getName(), e);
            } finally {
                LOG.fatal("Problem whilst executing statement " + command + " for upgrade " +  upgrade.getName(), e);
                System.exit(-1);
            }
        } finally {
            close(statement);
            close(connection);
        }
    }

    private void close(Statement statement) {
        try {
            statement.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void close(Connection connection) {
        try {
            connection.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
